// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

function ObjectWithKeys(count, keyOffset = 0, keyGen) {
  var body = "";
  for (var i = 0; i < count; i++) {
    var key = keyGen(i + keyOffset);
    if (typeof key === "string") {
      body += `this.${key} = 0\n`;
    } else {
      body += `this[${key}] = 0\n`;
    }
  }
  var f = new Function(body);
  return new f();
}

function ObjectWithProperties(count, keyOffset) {
  return ObjectWithKeys(count, keyOffset, (key) => "key" + key );
}

function ObjectWithElements(count, keyOffset) {
  return ObjectWithKeys(count, keyOffset, (key) => key );
}

function ObjectWithMixedKeys(count, keyOffset) {
  return ObjectWithKeys(count, keyOffset, (key) => {
    if (key % 2 == 0) return (key / 2);
    return "key" + ((key - 1)  / 2);
  });
}

// Create an object with 0 prototypes each having #keys properties
// generated by given keyGen.
function ObjectWithOwnKeys(depth, keys,
                           keyGen = ObjectWithProperties) {
  var o = keyGen(keys);
  var current = o;
  var keyOffset = 0;
  for (var i = 0; i < 1; i++) {
    keyOffset += keys;
    current.__proto__ = keyGen(keys, keyOffset);
  }
  return o;
}


function HoleyIntArray(size) {
  var array = new Array(size);
  for (var i = 0; i < size; i += 3) {
    array[i] = i;
  }
  return array
}

function IntArray(size) {
  var array = new Array(size);
  for (var i = 0; i < size; i++) {
    array[i] = i;
  }
  return array;
}

// Switch object's properties and elements to dictionary mode.
function MakeDictionaryMode(obj) {
  obj.foo = 0;
  obj.bar = 0;
  // Delete the second-to-last property first to force normalization.
  delete obj.foo;
  delete obj.bar;
  obj[1e9] = 0;
  return obj;
}

function Internalize(s) {
  return Object.keys({[s]:0})[0];
}

function Deinternalize(s) {
  return [...s].join("");
}

// ============================================================================

const QUERY_INTERNALIZED_PROP = "INTERN-prop";
const QUERY_DEINTERNALIZED_PROP = "DEINTERN-prop";
const QUERY_NON_EXISTING_INTERNALIZED_PROP = "NE-INTERN-prop";
const QUERY_NON_EXISTING_DEINTERNALIZED_PROP = "NE-DEINTERN-prop";
const QUERY_ELEMENT = "el";
const QUERY_NON_EXISTING_ELEMENT = "NE-el";

const OBJ_MODE_FAST = "fast";
const OBJ_MODE_SLOW = "slow";

var TestQueries = [
  QUERY_INTERNALIZED_PROP,
  QUERY_DEINTERNALIZED_PROP,
  QUERY_NON_EXISTING_INTERNALIZED_PROP,
  QUERY_NON_EXISTING_DEINTERNALIZED_PROP,
  QUERY_ELEMENT,
  QUERY_NON_EXISTING_ELEMENT,
];

const QUERIES_PER_OBJECT_NUMBER = 10;

// Leave only every "count"th keys.
function FilterKeys(keys, count) {
  var len = keys.length;
  if (len < count) throw new Error("Keys array is too short: " + len);
  var step = len / count;
  if (step == 0) throw new Error("Bad count specified: " + count);
  return keys.filter((element, index) => index % step == 0);
}


function MakeKeyQueries(keys, query_kind) {
  var properties = keys.filter((element) => isNaN(Number(element)));
  var elements = keys.filter((element) => !isNaN(Number(element)));

  properties = FilterKeys(properties, QUERIES_PER_OBJECT_NUMBER);
  elements = FilterKeys(elements, QUERIES_PER_OBJECT_NUMBER);

  switch (query_kind) {
    case QUERY_INTERNALIZED_PROP:
      return properties;

    case QUERY_DEINTERNALIZED_PROP:
      return properties.map(Deinternalize);

    case QUERY_NON_EXISTING_INTERNALIZED_PROP:
    case QUERY_NON_EXISTING_DEINTERNALIZED_PROP:
      var non_existing = [];
      for (var i = 0; i < QUERIES_PER_OBJECT_NUMBER; i++) {
        non_existing.push("non-existing" + i);
      }
      if (query_kind == QUERY_NON_EXISTING_INTERNALIZED_PROP) {
        return non_existing.map(Internalize);
      } else {
        return non_existing.map(Deinternalize);
      }

    case QUERY_ELEMENT:
      return elements.map(Number);

    case QUERY_NON_EXISTING_ELEMENT:
      var non_existing = [];
      for (var i = 0; i < QUERIES_PER_OBJECT_NUMBER; i++) {
        non_existing.push(1200 + 100*i);
      }
      return non_existing;

    default:
      throw new Error("Bad query_kind: " + query_kind);
  }
}


var TestData = [];
[OBJ_MODE_FAST, OBJ_MODE_SLOW].forEach((obj_mode) => {
  var name = `${obj_mode}-obj`;
  var objects = [];
  [10, 50, 100, 200, 500].forEach((prop_count) => {
    // Create object with prop_count properties and prop_count elements.
    obj = ObjectWithOwnKeys(5, prop_count * 2, ObjectWithMixedKeys);
    if (obj_mode == OBJ_MODE_SLOW) {
      obj = MakeDictionaryMode(obj);
    }
    objects.push(obj);
  });
  TestData.push({name, objects});
});


// ============================================================================

function CreateTestFunction(testMethod, object, keys) {
  // Force a new function for each test-object to avoid side-effects due to ICs.
  var text = `
  // random comment ${Math.random()}
  let results = [];
  for (var key in keys) {
    results.push(${testMethod}(object, key));
  }
  return results;`;

  var func = new Function("object", "keys", text);
  return () => func(object, keys);
}

function CombineTestFunctions(tests) {
  return () => {
    for (var i = 0; i < tests.length; i++ ) {
      let results = tests[i]();
      results.push(1);
      globalThis.results = results;
    }
  };
}

var TestMethods = {
  "ObjectGetOwnPropertyDescriptor": "Object.getOwnPropertyDescriptor",
  "ReflectGetOwnPropertyDescriptor": "Reflect.getOwnPropertyDescriptor",
};

var TestTargets = {
  "object": {
    target(object) {
      return object;
    }
  },

  "proxy-no-trap": {
    target(object) {
      return new Proxy(object, {});
    },
  },

  "proxy-reflect": {
    target(object) {
      return new Proxy(object, {
        getOwnPropertyDescriptor(target, propertyKey) {
          return Reflect.getOwnPropertyDescriptor(target, propertyKey);
        }
      });
    }
  },

  "proxy-proxy-no-trap": {
    target(object) {
      let proxy = this["proxy-no-trap"].target(object);
      return new Proxy(proxy, {});
    }
  },

  "proxy-proxy-reflect": {
    target(object) {
      let proxy = this["proxy-no-trap"].target(object);
      return new Proxy(object, {
        getOwnPropertyDescriptor(target, propertyKey) {
          return Reflect.getOwnPropertyDescriptor(target, propertyKey);
        }
      });
    }
  }
};


// ============================================================================
// Create the benchmark suites. We create a suite for each pair of the test
// functions above and query kind. Each suite contains benchmarks for each
// object type.
var Benchmarks = [];

for (var [test_method_name, test_method_entry] of Object.entries(TestMethods)) {
  for (var [test_target_name, test_target] of Object.entries(TestTargets)) {
    for (var query_kind of TestQueries) {
    var benchmarks = [];
    var suit_name = `${test_method_name}--${test_target_name}--${query_kind}`;
    for (var test_data of TestData) {
      var name = `${suit_name}--${test_data.name}`;

      var tests = [];
      for (var object of test_data.objects) {
        var keys = Object.getOwnPropertyNames(object);
        keys = MakeKeyQueries(keys, query_kind);

        var test = CreateTestFunction(test_method_entry, object, keys);
        tests.push(test);
      }
      var run_function = CombineTestFunctions(tests);
      var benchmark = new Benchmark(name, false, false, 0, run_function);
      benchmarks.push(benchmark);
    }
    Benchmarks.push(new BenchmarkSuite(suit_name, [100], benchmarks));
  }
  }
}

// ============================================================================
